use super::*;

pub(super) fn is_entity_interface(
    subgraphs: &subgraphs::Subgraphs,
    mut definitions: impl Iterator<Item = subgraphs::DefinitionId>,
) -> bool {
    // Take the first federation v2 definition.
    let Some(definition_id) = definitions.find(|def| {
        let subgraph_id = subgraphs.at(*def).subgraph_id;
        subgraphs.at(subgraph_id).federation_spec.is_apollo_v2()
    }) else {
        return false;
    };

    // Is it an entity interface object, or an interface with @key?
    let definition = &subgraphs.at(definition_id);

    match definition.kind {
        DefinitionKind::Object => definition.directives.interface_object(subgraphs),
        DefinitionKind::Interface => definition_id.keys(subgraphs).next().is_some(),
        _ => false,
    }
}

pub(crate) fn merge_entity_interface_definitions<'a>(
    ctx: &mut Context<'a>,
    first: DefinitionView<'a>,
    definitions: &[DefinitionView<'a>],
) {
    let interface_name = first.name;

    let interface_defs = || {
        definitions.iter().filter(|def| {
            def.kind == DefinitionKind::Interface
                && ctx
                    .subgraphs
                    .at(ctx.subgraphs.at(def.id).subgraph_id)
                    .federation_spec
                    .is_apollo_v2()
        })
    };
    let mut interfaces = interface_defs();

    let Some(interface_def) = interfaces.next() else {
        ctx.diagnostics.push_fatal(format!(
            "The entity interface `{}` is not defined as an interface in any subgraph.",
            ctx.subgraphs[first.name]
        ));
        return;
    };

    // More than one interface in subgraphs.
    if interfaces.next().is_some() {
        let all_implementers: BTreeSet<_> = interface_defs()
            .flat_map(|interface| {
                interface
                    .subgraph_id
                    .interface_implementers(ctx.subgraphs, interface_name)
                    .map(|def_id| ctx.subgraphs.at(def_id).name)
            })
            .collect();

        // All subsequent interfaces must have the same implementers.
        for interface in interface_defs() {
            let implementers: BTreeSet<_> = interface
                .subgraph_id
                .interface_implementers(ctx.subgraphs, interface_name)
                .map(|def_id| ctx.subgraphs.at(def_id).name)
                .collect();

            if implementers != all_implementers {
                let subgraph = ctx.subgraphs.at(interface.subgraph_id);
                let subgraph_name = ctx.subgraphs[subgraph.name].as_ref();
                let interface_name = ctx.subgraphs[interface_name].as_ref();
                let implementer_names = all_implementers
                    .difference(&implementers)
                    .map(|id| ctx.subgraphs[*id].as_ref())
                    .join(", ");
                ctx.diagnostics.push_fatal(format!(
                r#"[{subgraph_name}]: Interface type "{interface_name}" has a resolvable key in subgraph "{subgraph_name}" but that subgraph is missing some of the supergraph implementation types of "{interface_name}". Subgraph "{subgraph_name}" should define types {implementer_names}."#
                ));
            }

            if interface.directives.interface_object(ctx.subgraphs) {
                ctx.diagnostics.push_fatal(format!(
                    "[{}] The @interfaceObject directive is not valid on interfaces (on `{}`).",
                    ctx.subgraphs[ctx.subgraphs.at(interface.subgraph_id).name],
                    ctx.subgraphs[interface_name],
                ));
            }
        }
    }

    let description = interface_def.description.map(|d| ctx.subgraphs[d].as_ref());
    let interface_name = ctx.insert_string(interface_name);
    let directives = collect_composed_directives(definitions.iter().map(|def| def.directives), ctx);
    let interface_id = ctx.insert_interface(interface_name, description, directives);

    let Some(expected_key) = interface_def.id.keys(ctx.subgraphs).next() else {
        ctx.diagnostics.push_fatal(format!(
            "The entity interface `{}` is missing a key in the `{}` subgraph.",
            ctx.subgraphs[first.name],
            ctx.subgraphs[ctx.subgraphs.at(interface_def.subgraph_id).name],
        ));
        return;
    };

    ctx.insert_interface_resolvable_key(interface_id, expected_key, false);

    // Each object in other subgraphs has to have @interfaceObject and the same key as the entity interface.
    for definition in definitions.iter().filter(|def| def.kind == DefinitionKind::Object) {
        if !definition.directives.interface_object(ctx.subgraphs) {
            let definition_name = ctx.subgraphs[definition.name].as_ref();
            ctx.diagnostics.push_fatal(format!(
                "`{definition_name}` is an entity interface but the object type `{definition_name}` is missing the @interfaceObject directive in the `{}` subgraph.",
                ctx.subgraphs[ctx.subgraphs.at(definition.subgraph_id).name],
            ));
        }

        match definition.id.keys(ctx.subgraphs).next() {
            None => {
                ctx.diagnostics.push_fatal(format!(
                    "The object type `{}` is annotated with @interfaceObject but missing a key in the `{}` subgraph.",
                    ctx.subgraphs[first.name],
                    ctx.subgraphs[ctx.subgraphs.at(definition.subgraph_id).name]
                ));
            }
            Some(key) if key.fields() == expected_key.fields() => (),
            Some(_) => {
                ctx.diagnostics.push_fatal(format!(
                    "[{}] The object type `{}` is annotated with @interfaceObject but has a different key than the entity interface `{}`.",
                    ctx.subgraphs[ctx.subgraphs.at(definition.subgraph_id).name],
                    ctx.subgraphs[definition.name],
                    ctx.subgraphs[interface_def.name],
                ));
            }
        }

        for entity_key in definition.id.keys(ctx.subgraphs).filter(|key| key.is_resolvable()) {
            ctx.insert_interface_resolvable_key(interface_id, entity_key, true);
        }
    }

    let fields = object::compose_fields(ctx, definitions, interface_name);

    let fields_to_add: Vec<(subgraphs::StringId, _)> = fields
        .into_iter()
        .map(|mut field| {
            // Adding interface field.
            ctx.insert_field(field.clone());

            // Adding only the empty `@join__field` directive indicating it's coming from somewhere
            // else.
            field.directives = vec![ir::Directive::JoinEntityInterfaceField];
            (field.field_name, field)
        })
        .collect();

    // The fields of the entity interface are not only defined in the subgraph where the entity interface is an interface.
    // More fields are contributed by other subgraphs where there are objects with `@interfaceObject`. Those must be added now in all
    // the implementers of the interface as they won't have them in their definition.
    for object_id in interface_def
        .subgraph_id
        .interface_implementers(ctx.subgraphs, first.name)
    {
        let object = ctx.subgraphs.at(object_id);
        match object.id.keys(ctx.subgraphs).next() {
            Some(key) if key.selection_set == expected_key.fields() => (),
            Some(_) => ctx.diagnostics.push_fatal(format!(
                "[{}] The object type `{}` implements the entity interface `{}` but does not have the same key. The key must match exactly.",
                &ctx[ctx.subgraphs.at(object.subgraph_id).name],
                &ctx[object.name],
                ctx.subgraphs[first.name],
            )),
            None => ctx.diagnostics.push_fatal(format!(
                "[{}] The object type `{}` is annotated with @interfaceObject but missing a key.",
                &ctx[ctx.subgraphs.at(object.subgraph_id).name],
                &ctx[object.name],
            )),
        }

        let object_name = ctx.insert_string(object.name);

        let fields_to_add = fields_to_add
            .iter()
            // Avoid adding fields that are already present on the object by virtue of the object implementing the interface.
            .filter(|(name, _)| object.id.field_by_name(ctx.subgraphs, *name).is_none())
            .map(|(_, field_ir)| field_ir);

        for field_ir in fields_to_add {
            let mut field_ir = field_ir.clone();
            field_ir.parent_definition_name = object_name;
            ctx.insert_field(field_ir);
        }
    }
}
